<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>全能IPTV播放器 V1.0.0</title>
    <!-- 核心 JS 库 (未改变) -->
    <script src="https://cdn.jsdmirror.com/npm/hls.js@latest"></script>
    <script src="https://cdn.jsdmirror.com/npm/flv.js@latest"></script>
    
    <!-- 美化库 -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
    <script src="https://unpkg.com/@popperjs/core@2"></script>
    <script src="https://unpkg.com/tippy.js@6"></script>

    <style>
        :root {
            /* 默认暗黑主题变量 (New Palette) */
            --primary-color: #2563eb;
            --primary-hover: #598EF3;
            --secondary-color: #d946ef;
            --secondary-hover: #e16cf1; /* Manually derived */
            --success-color: #28a745;
            --danger-color: #dc3545;
            --warning-color: #ffc107;
            --info-color: #598EF3;
            --text-color: #cbd5e1;
            --text-muted: #94a3b8;
            --text-on-warning: #111;
            --bg-color: #1e293b;
            --surface-color: #334155;
            --border-color: #475569;
            --shadow-color: rgba(0, 0, 0, 0.5);
            --glass-bg: rgba(51, 65, 85, 0.75);
            --input-bg: rgba(203, 213, 225, 0.08);
            --backdrop-blur: 10px;
            --border-radius: 8px;
            --transition-speed: 0.3s;
        }

        body[data-theme="light"] {
            /* 日间主题变量 (New Palette) */
            --primary-color: #3F51B5;
            --primary-hover: #757de8;
            --secondary-color: #2196F3;
            --secondary-hover: #003f8f;
            --info-color: #2196F3;
            --text-color: #333333;
            --text-muted: #5c5c5c;
            --bg-color: #f5f5f5;
            --surface-color: #ffffff;
            --border-color: #cccccc;
            --shadow-color: rgba(0, 0, 0, 0.1);
            --glass-bg: rgba(255, 255, 255, 0.85);
            --input-bg: #ffffff;
        }

        @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap');
        * { box-sizing: border-box; }

        body {
            font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background-color: var(--bg-color); color: var(--text-color);
            margin: 0; padding: 0; display: flex; flex-direction: column;
            height: 100vh; overflow: hidden; transition: background-color var(--transition-speed), color var(--transition-speed);
        }

        .header {
            background: var(--glass-bg); backdrop-filter: blur(var(--backdrop-blur)); -webkit-backdrop-filter: blur(var(--backdrop-blur));
            padding: 15px 25px; border-bottom: 1px solid var(--border-color); box-shadow: 0 4px 15px var(--shadow-color);
            flex-shrink: 0; z-index: 10; transition: background-color var(--transition-speed), border-color var(--transition-speed);
        }
        .header-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px; }
        .title-group { display: flex; align-items: center; gap: 15px; }
        .header h1 { margin: 0; font-size: 1.6em; font-weight: 600; background: linear-gradient(45deg, var(--primary-color), var(--info-color)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
        
        #theme-toggle-btn {
            background: none; border: none; color: var(--text-muted); font-size: 1.4em; cursor: pointer;
            padding: 5px; line-height: 1; transition: color var(--transition-speed);
        }
        #theme-toggle-btn:hover { color: var(--primary-color); }
        
        #status-container { display: flex; gap: 8px; }
        .status-item { font-size: 0.85em; padding: 4px 10px; border-radius: 15px; background-color: var(--border-color);
            text-align: center; font-weight: 500; transition: all var(--transition-speed) ease; }
        #playbackIndicator { min-width: 90px; }

        .header-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
        .control-group { display: flex; align-items: center; }
        .source-group { flex-grow: 1; min-width: 300px; display: flex; }

        .select-wrapper { position: relative; display: flex; align-items: center; }
        .select-wrapper::after {
            content: '\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900;
            position: absolute; right: 15px; top: 50%; transform: translateY(-50%);
            color: var(--text-muted); pointer-events: none; transition: color var(--transition-speed);
        }
        #sourceSelector {
            -webkit-appearance: none; appearance: none;
            background-color: var(--input-bg); color: var(--text-color);
            border: 1px solid var(--border-color); border-right: none;
            border-radius: var(--border-radius) 0 0 var(--border-radius);
            padding: 10px 40px 10px 15px; font-size: 1em; width: auto; max-width: 200px;
            cursor: pointer; transition: all var(--transition-speed) ease;
        }

        #sourceUrlInput, #proxyUrlInput {
            padding: 10px 15px; border: 1px solid var(--border-color); background-color: var(--input-bg);
            color: var(--text-color); font-size: 1em; transition: all var(--transition-speed) ease;
        }
        #sourceSelector:focus, #sourceUrlInput:focus, #proxyUrlInput:focus {
            background-color: transparent; border-color: var(--primary-color);
            outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 25%, transparent);
        }
        #sourceUrlInput { flex-grow: 1; border-radius: 0 var(--border-radius) var(--border-radius) 0; }
        #proxyUrlInput { border-radius: var(--border-radius); }

        .btn {
    	    margin: 2px 5px;
            padding: 10px 15px; border: none; border-radius: var(--border-radius); cursor: pointer;
            font-size: 1em; font-weight: 500; color: white;
            transition: all var(--transition-speed) ease; display: flex; align-items: center; gap: 8px;
            background-color: var(--primary-color); white-space: nowrap;
        }
        .btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 8px var(--shadow-color); }
        .btn:active:not(:disabled) { transform: translateY(0); box-shadow: none; }
        .btn:disabled { background-color: #555; color: #999; cursor: not-allowed; }

        #loadListBtn { background-color: var(--primary-color); }
        #playDirectBtn { background-color: var(--warning-color); color: var(--text-on-warning); }
        #loadLocalLabel { background-color: var(--success-color); }
        #saveSourceBtn { background-color: var(--secondary-color); }
        #deleteSourceBtn { background-color: var(--danger-color); }
        #localFileInput { display: none; }

        .container { display: flex; flex-grow: 1; overflow: hidden; position: relative; }
        #playlist { width: 350px; background-color: var(--surface-color); border-right: 1px solid var(--border-color);
            flex-shrink: 0; display: flex; flex-direction: column;
            transition: transform var(--transition-speed) ease, background-color var(--transition-speed), border-color var(--transition-speed); }
        #playlist-controls {
            padding: 15px; border-bottom: 1px solid var(--border-color); background: var(--glass-bg);
            backdrop-filter: blur(var(--backdrop-blur)); transition: border-color var(--transition-speed), background-color var(--transition-speed);
        }
        #search-container { display: flex; gap: 10px; margin-bottom: 15px; }
        #searchInput { width: 100%; background-color: var(--input-bg); flex-grow: 1; border-radius: var(--border-radius); border: 1px solid var(--border-color); }
        #checkAvailabilityBtn, #exportBtn { flex-shrink: 0; padding: 10px; }
        #checkAvailabilityBtn { background-color: #6f42c1; } /* Keeping this distinct purple for check action */
        #exportBtn { background-color: var(--info-color); }
        #checkAvailabilityBtn .btn-text, #exportBtn .btn-text { display: none; }
        #filter-container { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
        .filter-btn { padding: 8px 10px; font-size: 0.9em; justify-content: center; background-color: var(--border-color); color: var(--text-muted); }
        .filter-btn.active { background-color: var(--primary-color); color: white; font-weight: 600; }
        #channel-list { overflow-y: auto; flex-grow: 1; position: relative; }
        #channel-list::-webkit-scrollbar { width: 8px; }
        #channel-list::-webkit-scrollbar-track { background: var(--surface-color); }
        #channel-list::-webkit-scrollbar-thumb { background-color: #555; border-radius: 4px; border: 2px solid var(--surface-color); }
        #channel-list::-webkit-scrollbar-thumb:hover { background-color: #777; }

        .channel-item {
            display: flex; justify-content: space-between; align-items: center;
            padding: 12px 18px; cursor: pointer; border-bottom: 1px solid var(--border-color);
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.95em;
            transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease, border-color var(--transition-speed);
        }
        .channel-name { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; }
        .channel-status { margin-left: 10px; font-size: 0.85em; font-weight: bold; flex-shrink: 0; padding: 2px 6px; border-radius: 4px; }
        .status-available { color: var(--success-color); }
        .status-unavailable { color: var(--danger-color); }
        .status-checking { color: var(--warning-color); }
        .channel-item:hover { background-color: var(--primary-hover); color: white; }
        .channel-item.active { background: linear-gradient(90deg, var(--primary-color), var(--primary-hover)); color: white; font-weight: 600; }
        
        .loader { width: 48px; height: 48px; border: 3px solid var(--text-color); border-bottom-color: transparent; border-radius: 50%; display: none; animation: rotation 1s linear infinite; position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%); }
        @keyframes rotation { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

        #video-container { flex-grow: 1; background-color: #000; display: flex; justify-content: center; align-items: center; }
        #videoPlayer { width: 100%; height: 100%; }
        .status-message { padding: 40px 20px; text-align: center; color: var(--text-muted); font-size: 1.1em; }
        #playlist-toggle { display: none; }

        @media (max-width: 992px) { /* Tablet and below */
            .header-controls { row-gap: 15px; }
            .proxy-group { width: 100%; order: 3; }
            #proxyUrlInput { width: 100%; }
            .management-group { order: 2; }
            .actions-group { order: 1; }
        }

        @media (max-width: 768px) { /* Mobile */
            body { overflow: auto; }
            .header { padding: 15px; }
            h1 { font-size: 1.4em !important; }
            .status-item { font-size: 0.75em; padding: 3px 8px; }
            #playbackIndicator { min-width: 75px; }
            .header-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
            .control-group, .source-group { width: 100%; }
            .source-group { grid-column: 1 / -1; }
            .actions-group, .management-group, .proxy-group { display: contents; }
            .btn span { display: none; }
            .btn { justify-content: center; padding: 12px; }
            #checkAvailabilityBtn .btn-text, #exportBtn .btn-text { display: block; }
            #checkAvailabilityBtn, #exportBtn { padding: 10px 15px; }

            .container { flex-direction: column; overflow: visible; }
            #video-container { order: 1; height: 56.25vw; min-height: 210px; width: 100%; }
            #playlist {
                position: fixed; bottom: 0; left: 0; right: 0; width: 100%; height: 60vh;
                transform: translateY(100%); border-right: none; border-top: 2px solid var(--primary-color);
                z-index: 1000; box-shadow: 0 -5px 20px var(--shadow-color);
            }
            #playlist.open { transform: translateY(0); }

            #playlist-toggle {
                display: flex; position: fixed; bottom: 60px; right: 20px;
                width: 50px; height: 50px; border-radius: 50%; background-color: var(--primary-color);
                color: white; justify-content: center; align-items: center; font-size: 1.5em;
                z-index: 1001; box-shadow: 0 4px 10px var(--shadow-color); border: none; cursor: pointer;
            }
		#sourceUrlInput {width: 1.5rem;}
        }
    </style>
</head>
<body data-theme="dark">
    <div class="header">
        <div class="header-top">
            <div class="title-group">
                <h1><i class="fa-solid fa-tv"></i> IPTV Player</h1>
                <button id="theme-toggle-btn" data-tippy-content="切换主题"><i class="fa-solid fa-sun"></i></button>
            </div>
            <div id="status-container">
                <span id="modeIndicator" class="status-item"></span>
                <span id="playbackIndicator" class="status-item"></span>
                <span id="checkIndicator" class="status-item"></span>
            </div>
        </div>
        <div class="header-controls">
            <div class="control-group source-group">
                <div class="select-wrapper">
                    <select id="sourceSelector" data-tippy-content="选择一个预设或自定义的源"></select>
                </div>
                <input type="text" id="sourceUrlInput" placeholder="选择或输入源 URL">
            </div>
            <div class="control-group actions-group">
                <button id="loadListBtn" class="btn" data-tippy-content="从URL加载频道列表"><i class="fa-solid fa-cloud-arrow-down"></i><span>加载</span></button>
                <button id="playDirectBtn" class="btn" data-tippy-content="直接播放输入的URL"><i class="fa-solid fa-play"></i><span>播放</span></button>
                <label for="localFileInput" id="loadLocalLabel" class="btn" data-tippy-content="加载本地 M3U 文件"><i class="fa-solid fa-folder-open"></i><span>本地</span></label>
                <input type="file" id="localFileInput" accept=".m3u,.m3u8">
            </div>
            <div class="control-group management-group">
                <button id="saveSourceBtn" class="btn" data-tippy-content="将当前URL保存为自定义源"><i class="fa-solid fa-floppy-disk"></i><span>保存</span></button>
                <button id="deleteSourceBtn" class="btn" disabled data-tippy-content="删除选中的自定义源"><i class="fa-solid fa-trash-can"></i><span>删除</span></button>
            </div>
            <div class="control-group proxy-group"><input type="text" id="proxyUrlInput" placeholder="代理地址 (可选)" data-tippy-content="为所有网络请求设置代理"></div>
        </div>
    </div>
    <div class="container">
        <div id="playlist">
            <div id="playlist-controls">
                <div id="search-container">
                    <input type="text" id="searchInput" placeholder="搜索频道...">
                    <button id="checkAvailabilityBtn" class="btn" data-tippy-content="检测频道可用性"><i class="fa-solid fa-check-double"></i><span class="btn-text">检测</span></button>
                    <button id="exportBtn" class="btn" data-tippy-content="导出频道列表"><i class="fa-solid fa-file-export"></i><span class="btn-text">导出</span></button>
                </div>
                <div id="filter-container">
                    <button id="filterAllBtn" class="filter-btn btn active">全部</button>
                    <button id="filterAvailableBtn" class="filter-btn btn">可用</button>
                    <button id="filterUnavailableBtn" class="filter-btn btn">无效</button>
                </div>
            </div>
            <div id="channel-list">
                <div class="loader"></div>
                <div class="status-message">请选择源, 或加载本地文件</div>
            </div>
        </div>
        <div id="video-container"><video id="videoPlayer" controls autoplay></video></div>
    </div>
    
    <button id="playlist-toggle" data-tippy-content="切换播放列表"><i class="fa-solid fa-list-ul"></i></button>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            // Element variables...
            const body = document.body;
            const themeToggleBtn = document.getElementById('theme-toggle-btn');
            const modeIndicator = document.getElementById('modeIndicator');
            const playbackIndicator = document.getElementById('playbackIndicator');
            const checkIndicator = document.getElementById('checkIndicator');
            const sourceSelector = document.getElementById('sourceSelector');
            const sourceUrlInput = document.getElementById('sourceUrlInput');
            const proxyUrlInput = document.getElementById('proxyUrlInput');
            const saveSourceBtn = document.getElementById('saveSourceBtn');
            const deleteSourceBtn = document.getElementById('deleteSourceBtn');
            const loadListBtn = document.getElementById('loadListBtn');
            const playDirectBtn = document.getElementById('playDirectBtn');
            const localFileInput = document.getElementById('localFileInput');
            const searchInput = document.getElementById('searchInput');
            const channelListDiv = document.getElementById('channel-list');
            const videoElement = document.getElementById('videoPlayer');
            const checkAvailabilityBtn = document.getElementById('checkAvailabilityBtn');
            const exportBtn = document.getElementById('exportBtn');
            const filterAllBtn = document.getElementById('filterAllBtn');
            const filterAvailableBtn = document.getElementById('filterAvailableBtn');
            const filterUnavailableBtn = document.getElementById('filterUnavailableBtn');
            const playlistDiv = document.getElementById('playlist');
            const playlistToggleBtn = document.getElementById('playlist-toggle');
            const loader = document.querySelector('.loader');

            // State variables...
            const STORAGE_KEY_SOURCES = 'iptv_custom_sources_v2';
            const STORAGE_KEY_THEME = 'iptv_theme_v1';
            const MAX_RETRIES = 10;
            const DEFAULT_SOURCES = [
                { name: 'Guovin (主力)', url: 'https://cdn.jsdmirror.com/gh/Guovin/iptv-api@gd/output/result.m3u' },
                { name: 'iptv-org (国际)', url: 'https://cdn.jsdmirror.com/gh/iptv-org/iptv@gh-pages/index.m3u' },
                { name: 'AKTV', url: 'https://vercel-api-proxy-taupe-chi.vercel.app/https/aktv.space/live.m3u' },
                { name: 'Moexin(NSFW)', url: 'https://cdn.jsdmirror.com/gh/Moexin/IPTV@main/IPTV.m3u' },
                { name: 'Rivens7', url: 'https://cdn.jsdmirror.com/gh/Rivens7/Livelist@main/IPTV.m3u' },
            ];
            let hls = null, flvPlayer = null, currentActiveChannel = null, allChannels = [];
            let currentPlayingUrl = null, isCurrentStreamLive = false, retryCount = 0;
            let isChecking = false;
            let currentFilter = 'all'; 
            let cancellationToken = { cancelled: false };
            let userPaused = false;
            let stallTimer = null;

            function updateModeStatus(text, color = '#6c757d') { modeIndicator.textContent = text; modeIndicator.style.backgroundColor = color; }
            function updateCheckStatus(text, color = '#6c757d') { checkIndicator.textContent = text; checkIndicator.style.backgroundColor = color; }
            function updatePlaybackStatus(text, color = '#6c757d') { playbackIndicator.textContent = text; playbackIndicator.style.backgroundColor = color; }

            function getProxyPrefix() { let p = proxyUrlInput.value.trim(); if (p && !p.endsWith('/')) { p += ''; } return p; }
            
            function buildProxiedUrl(targetUrl, proxyPrefix) {
                if (!proxyPrefix || !targetUrl.startsWith('http')) {
                    return targetUrl;
                }
                return proxyPrefix + encodeURIComponent(targetUrl);
            }

            function clearPlaylist() { 
                allChannels = []; currentFilter = 'all'; updateFilterButtons(); searchInput.value = '';
                const statusMessage = '<div class="status-message">请选择源, 或加载本地文件</div>';
                channelListDiv.innerHTML = statusMessage; 
                const newLoader = document.createElement('div'); newLoader.className = 'loader';
                channelListDiv.prepend(newLoader);
                if(currentActiveChannel) { currentActiveChannel.classList.remove('active'); currentActiveChannel=null; }
            };
            function getCustomSources() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_SOURCES)) || []; } catch (e) { return []; } }
            function saveCustomSources(sources) { localStorage.setItem(STORAGE_KEY_SOURCES, JSON.stringify(sources)); }
            function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }

            // --- MODIFIED CODE START ---
            function playChannel(url) {
                if (currentPlayingUrl !== url) { retryCount = 0; }
                currentPlayingUrl = url; 
                isCurrentStreamLive = false;
                userPaused = false;
                
                if (stallTimer) clearTimeout(stallTimer);
                
                // Detach and destroy previous players
                if (hls) { hls.destroy(); hls = null; }
                if (flvPlayer) { flvPlayer.destroy(); flvPlayer = null; }
                // For native playback, setting a new src is enough, but ensure we clear it if the previous was native
                videoElement.src = '';
                videoElement.removeAttribute('src');

                const streamUrl = buildProxiedUrl(url, getProxyPrefix());
                updatePlaybackStatus('连接中', 'var(--primary-color)');
                console.log(`Attempting to play: ${streamUrl}`);
                
                const lcUrl = streamUrl.toLowerCase();
                if (lcUrl.endsWith('.m3u8') || lcUrl.includes('.m3u8?')) {
                    playHls(streamUrl);
                } else if (lcUrl.endsWith('.flv') || lcUrl.includes('.flv?')) {
                    playFlv(streamUrl);
                } else if (lcUrl.endsWith('.mp4') || lcUrl.includes('.mp4?') ||
                           lcUrl.endsWith('.webm') || lcUrl.includes('.webm?') ||
                           lcUrl.endsWith('.mkv') || lcUrl.includes('.mkv?') ||
                           lcUrl.endsWith('.mov') || lcUrl.includes('.mov?') ||
                           lcUrl.endsWith('.ogg') || lcUrl.includes('.ogg?')) {
                    playNative(streamUrl);
                } else {
                    // Default fallback for streams without a clear extension (common in IPTV)
                    playHls(streamUrl);
                }
                
                if (window.innerWidth <= 768) { 
                    playlistDiv.classList.remove('open'); 
                    playlistToggleBtn.innerHTML = '<i class="fa-solid fa-list-ul"></i>'; 
                }
            }
            // --- MODIFIED CODE END ---

            // --- NEW FUNCTION START ---
            function playNative(streamUrl) {
                isCurrentStreamLive = false; // Regular files are not live streams
                videoElement.src = streamUrl;
                videoElement.load();
                videoElement.play().catch(e => {
                    console.error("Autoplay was prevented for native video:", e);
                    updatePlaybackStatus('需要用户交互', 'var(--warning-color)');
                });
            }
            // --- NEW FUNCTION END ---
            
            function playFlv(streamUrl) {
                isCurrentStreamLive = true;
                if (flvjs.isSupported()) {
                    flvPlayer = flvjs.createPlayer({
                        type: 'flv',
                        isLive: true,
                        url: streamUrl,
                        config: {
                            enableStashBuffer: true,
                            stashInitialSize: 2048,
                            autoCleanupSourceBuffer: true
                        }
                    });
                    flvPlayer.attachMediaElement(videoElement);
                    flvPlayer.load();
                    flvPlayer.play().catch(e => console.error("Autoplay prevented:", e));

                    flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => {
                        if (!isCurrentStreamLive || userPaused) return;
                        console.error(`FLV.js Error: ${errorType}. Details: ${errorDetail}. Reconnecting...`);
                        if (flvPlayer) { flvPlayer.destroy(); flvPlayer = null; }

                        if (retryCount < MAX_RETRIES) {
                            retryCount++;
                            const delay = Math.min(Math.pow(2, retryCount) * 200, 5000);
                            updatePlaybackStatus(`错误, 重试 ${retryCount}`, 'orange');
                            setTimeout(() => {
                                if (currentPlayingUrl === streamUrl && !userPaused) {
                                    updatePlaybackStatus('重连中', 'var(--primary-color)');
                                    playChannel(currentPlayingUrl);
                                }
                            }, delay);
                        } else {
                            updatePlaybackStatus('重连失败', 'var(--danger-color)');
                        }
                    });
                } else {
                    alert('您的浏览器不支持 FLV 播放。');
                }
            }
            
            function playHls(streamUrl) {
                if (Hls.isSupported()) {
                    const hlsConfig = { manifestLoadingMaxRetry: 0, fragLoadingMaxRetry: 0, manifestLoadingTimeOut: 10000, fragLoadingTimeOut: 15000, maxBufferLength: 60, maxBufferSize: 60 * 1000 * 1000, backBufferLength: 90, maxBufferHole: 20, };
                    hls = new Hls(hlsConfig);
                    hls.loadSource(streamUrl);
                    hls.attachMedia(videoElement);
                    hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => { isCurrentStreamLive = data.details.live; videoElement.play().catch(e => {}); });
                    hls.on(Hls.Events.ERROR, (event, data) => {
                        if (data.fatal) {
                            switch (data.type) {
                                case Hls.ErrorTypes.NETWORK_ERROR:
                                    if (retryCount < MAX_RETRIES) {
                                        retryCount++;
                                        const delay = Math.min(Math.pow(2, retryCount) * 100 + Math.random() * 100, 2000);
                                        updatePlaybackStatus(`错误, 重试${retryCount}`, 'orange');
                                        setTimeout(() => {
                                            if (hls && !userPaused) {
                                                updatePlaybackStatus('重连中', 'var(--primary-color)');
                                                hls.startLoad();
                                            }
                                        }, delay);
                                    } else if (!userPaused) {
                                        updatePlaybackStatus('重连失败', 'var(--danger-color)');
                                        playChannel(streamUrl);
                                    }
                                    break;
                                case Hls.ErrorTypes.MEDIA_ERROR:
                                    if (!userPaused) hls.recoverMediaError();
                                    break;
                                default:
                                    updatePlaybackStatus('播放失败', 'var(--danger-color)');
                                    if(hls) hls.destroy();
                                    break;
                            }
                        }
                    });
                } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
                    isCurrentStreamLive = true;
                    videoElement.src = streamUrl;
                    videoElement.addEventListener('loadedmetadata', () => videoElement.play().catch(e => {}));
                } else {
                    alert('您的浏览器不支持 HLS 或 TS 播放。');
                }
            }
            
            function renderPlaylist(channelsToRender) {
                const loader = channelListDiv.querySelector('.loader');
                if (loader) loader.style.display = 'none';
                
                Array.from(channelListDiv.children).forEach(child => { if (child.classList.contains('channel-item') || child.classList.contains('status-message')) { channelListDiv.removeChild(child); } });

                if (channelsToRender.length === 0) { channelListDiv.insertAdjacentHTML('beforeend', '<div class="status-message">无匹配频道</div>'); return; }
                const fragment = document.createDocumentFragment();
                channelsToRender.forEach(channel => {
                    const channelDiv = document.createElement('div'); channelDiv.className = 'channel-item';
                    const channelNameSpan = document.createElement('span'); channelNameSpan.className = 'channel-name'; channelNameSpan.textContent = channel.name || '未知频道'; channelNameSpan.title = channel.name;
                    const channelStatusSpan = document.createElement('span'); channelStatusSpan.className = 'channel-status';
                    switch(channel.status) {
                        case 'available': channelStatusSpan.textContent = '可用'; channelStatusSpan.classList.add('status-available'); break;
                        case 'unavailable': channelStatusSpan.textContent = '无效'; channelStatusSpan.classList.add('status-unavailable'); break;
                        case 'checking': channelStatusSpan.textContent = '检测中'; channelStatusSpan.classList.add('status-checking'); break;
                    }
                    channelDiv.appendChild(channelNameSpan);
                    if (channel.status !== 'unchecked') { channelDiv.appendChild(channelStatusSpan); }
                    channelDiv.dataset.url = channel.url;
                    channelDiv.addEventListener('click', () => {
                        playChannel(channel.url);
                        if (currentActiveChannel) { currentActiveChannel.classList.remove('active'); }
                        channelDiv.classList.add('active'); currentActiveChannel = channelDiv;
                        channelDiv.scrollIntoView({ behavior: "smooth", block: "nearest" });
                    });
                    fragment.appendChild(channelDiv);
                });
                channelListDiv.appendChild(fragment);
            }

            function updateChannelDisplay() { let channelsToDisplay = allChannels; if (currentFilter !== 'all') { channelsToDisplay = channelsToDisplay.filter(c => c.status === currentFilter); } const searchText = searchInput.value.toLowerCase().trim(); if (searchText) { channelsToDisplay = channelsToDisplay.filter(c => c.name.toLowerCase().includes(searchText)); } renderPlaylist(channelsToDisplay); }
            async function loadM3UFromUrl() { const url = sourceUrlInput.value.trim(); if (!url) { alert('请选择或输入 M3U 列表的 URL'); return; } clearPlaylist(); updateModeStatus('URL列表模式', '#17a2b8'); const loader = channelListDiv.querySelector('.loader'); if (loader) loader.style.display = 'block'; channelListDiv.querySelector('.status-message')?.remove(); const proxyPrefix = getProxyPrefix(); const fetchUrl = buildProxiedUrl(url, proxyPrefix); try { const response = await fetch(fetchUrl); if (!response.ok) { throw new Error(`HTTP 错误! 状态: ${response.status}`); } const m3uText = await response.text(); processM3UContent(m3uText); } catch (error) { updateModeStatus('加载失败', '#dc3545'); if (loader) loader.style.display = 'none'; channelListDiv.insertAdjacentHTML('beforeend', `<div class="status-message" style="color: red;">加载失败: ${error.message}</div>`); } }
            function processM3UContent(m3uText) { allChannels = parseM3U(m3uText); if (allChannels.length === 0) { const loader = channelListDiv.querySelector('.loader'); if (loader) loader.style.display = 'none'; channelListDiv.insertAdjacentHTML('beforeend','<div class="status-message">未找到有效频道。</div>'); return; } updateChannelDisplay(); }
            function parseM3U(m3uText) { const lines = m3uText.split('\n'); const channels = []; let currentChannel = {}; const extinfRegex = /#EXTINF:.*,(.+)/; for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('#EXTINF:')) { const match = trimmedLine.match(extinfRegex); if (match && match[1]) { currentChannel = { name: match[1].trim(), status: 'unchecked' }; } } else if (trimmedLine && !trimmedLine.startsWith('#')) { if (currentChannel.name) { currentChannel.url = trimmedLine; channels.push(currentChannel); currentChannel = {}; } } } return channels; }
            function toggleCheckAvailability() { if (isChecking) { cancellationToken.cancelled = true; updateCheckStatus('正在停止...', 'var(--warning-color)'); checkAvailabilityBtn.disabled = true; } else { checkChannelAvailability(); } }
            async function checkChannelAvailability() { if (allChannels.length === 0) { alert('频道列表为空，无法检测。'); return; } isChecking = true; cancellationToken = { cancelled: false }; checkAvailabilityBtn.innerHTML = '<i class="fa-solid fa-stop"></i><span class="btn-text">停止</span>'; checkAvailabilityBtn.style.backgroundColor = 'var(--danger-color)'; const proxyPrefix = getProxyPrefix(); const totalChannels = allChannels.length; let checkedCount = 0; const CONCURRENCY_LIMIT = 10; const channelsToCheck = [...allChannels]; const worker = async () => { while (channelsToCheck.length > 0) { if (cancellationToken.cancelled) { break; } const channel = channelsToCheck.shift(); if (!channel) { break; } channel.status = 'checking'; updateChannelDisplay(); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); try { let urlToCheck = channel.url; if (channel.url.toLowerCase().includes('.m3u')) { const masterUrl = buildProxiedUrl(channel.url, proxyPrefix); const response = await fetch(masterUrl, { signal: controller.signal, mode: 'cors' }); if (!response.ok) throw new Error(`Master M3U fetch failed: ${response.status}`); const m3uText = await response.text(); const firstStreamLine = m3uText.split('\n').find(line => line.trim() && !line.trim().startsWith('#')); if (firstStreamLine) { urlToCheck = new URL(firstStreamLine, channel.url).href; } else { throw new Error('Master M3U has no stream URLs'); } } const finalCheckUrl = buildProxiedUrl(urlToCheck, proxyPrefix); const finalResponse = await fetch(finalCheckUrl, { signal: controller.signal, mode: 'cors' }); channel.status = finalResponse.ok ? 'available' : 'unavailable'; } catch (error) { channel.status = 'unavailable'; } finally { clearTimeout(timeoutId); checkedCount++; if (!cancellationToken.cancelled) { updateCheckStatus(`检测中 (${checkedCount}/${totalChannels})`, 'orange'); } updateChannelDisplay(); } } }; const workerPromises = Array.from({ length: CONCURRENCY_LIMIT }, () => worker()); await Promise.all(workerPromises); isChecking = false; checkAvailabilityBtn.disabled = false; checkAvailabilityBtn.innerHTML = '<i class="fa-solid fa-check-double"></i><span class="btn-text">检测</span>'; checkAvailabilityBtn.style.backgroundColor = '#6f42c1'; if (cancellationToken.cancelled) { updateCheckStatus('检测已停止', 'var(--warning-color)'); } else { updateCheckStatus('检测完成', 'var(--success-color)'); } updateChannelDisplay(); }
            function handleExport() { if (allChannels.length === 0) { alert('频道列表为空，无法导出。'); return; } const choice = prompt("请选择导出类型:\n1. 导出全部频道\n2. 仅导出可用频道", "2"); if (choice === null) { return; } let channelsToExport, filename; if (choice === '1') { channelsToExport = allChannels; filename = 'playlist_all.m3u'; } else if (choice === '2') { channelsToExport = allChannels.filter(c => c.status === 'available'); filename = 'playlist_available.m3u'; if (channelsToExport.length === 0) { alert('没有可用的频道可以导出。请先运行检测。'); return; } } else { alert('无效的选择。'); return; } const m3uContent = generateM3UContent(channelsToExport); downloadM3UFile(m3uContent, filename); }
            function generateM3UContent(channels) { let content = '#EXTM3U\n'; channels.forEach(channel => { content += `#EXTINF:-1,${channel.name}\n`; content += `${channel.url}\n`; }); return content; }
            function downloadM3UFile(content, filename) { const blob = new Blob([content], { type: 'audio/mpegurl;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }
            function updateFilterButtons() { document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active')); if (currentFilter === 'all') filterAllBtn.classList.add('active'); if (currentFilter === 'available') filterAvailableBtn.classList.add('active'); if (currentFilter === 'unavailable') filterUnavailableBtn.classList.add('active'); }
            function handleFilterChange(newFilter) { currentFilter = newFilter; updateFilterButtons(); updateChannelDisplay(); }
            function handleDirectPlay() { const url = sourceUrlInput.value.trim(); if (!url) { alert('请输入要直接播放的流 URL'); return; } clearPlaylist(); updateModeStatus('直接播放', 'var(--warning-color)'); playChannel(url); }
            function handleFileSelect(e) { const f = e.target.files[0]; if (!f) return; clearPlaylist(); updateModeStatus('本地文件', 'var(--success-color)'); const loader = channelListDiv.querySelector('.loader'); if (loader) loader.style.display = 'block'; channelListDiv.querySelector('.status-message')?.remove(); const r = new FileReader(); r.onload = (ev) => processM3UContent(ev.target.result); r.readAsText(f); e.target.value = ''; }
            function handleSearch() { updateChannelDisplay(); }
            function handleSourceSelect() { const selectedOption = sourceSelector.options[sourceSelector.selectedIndex]; sourceUrlInput.value = sourceSelector.value; deleteSourceBtn.disabled = !selectedOption.dataset.custom; }
            function populateSourceSelector() { const customSources = getCustomSources(); const currentVal = sourceSelector.value; sourceSelector.innerHTML = ''; const defaultGroup = document.createElement('optgroup'); defaultGroup.label = '默认源'; DEFAULT_SOURCES.forEach(source => { const o = document.createElement('option'); o.value = source.url; o.textContent = source.name; defaultGroup.appendChild(o); }); sourceSelector.appendChild(defaultGroup); if (customSources.length > 0) { const customGroup = document.createElement('optgroup'); customGroup.label = '自定义源'; customSources.forEach(source => { const o = document.createElement('option'); o.value = source.url; o.textContent = source.name; o.dataset.custom = 'true'; customGroup.appendChild(o); }); sourceSelector.appendChild(customGroup); } sourceSelector.value = currentVal || DEFAULT_SOURCES[0].url; handleSourceSelect(); }
            function saveCustomSource() { const url = sourceUrlInput.value.trim(); if (!url) { alert('URL 不能为空'); return; } const name = prompt('请输入此源的名称:', '我的自定义源'); if (!name) return; const sources = getCustomSources(); if (sources.some(s => s.url === url)) { alert('此 URL 已存在'); return; } sources.push({ name, url }); saveCustomSources(sources); populateSourceSelector(); sourceSelector.value = url; handleSourceSelect(); alert('源已保存!'); }
            function deleteCustomSource() { const urlToDelete = sourceSelector.value; if (!confirm(`确定要删除源 "${sourceSelector.options[sourceSelector.selectedIndex].textContent}" 吗?`)) return; let sources = getCustomSources(); sources = sources.filter(s => s.url !== urlToDelete); saveCustomSources(sources); populateSourceSelector(); alert('源已删除!'); }
            function handlePlaylistToggle() { playlistDiv.classList.toggle('open'); this.innerHTML = playlistDiv.classList.contains('open') ? '<i class="fa-solid fa-xmark"></i>' : '<i class="fa-solid fa-list-ul"></i>'; }

            function applyTheme(theme) {
                body.dataset.theme = theme;
                themeToggleBtn.innerHTML = theme === 'dark' ? '<i class="fa-solid fa-sun"></i>' : '<i class="fa-solid fa-moon"></i>';
                localStorage.setItem(STORAGE_KEY_THEME, theme);
            }
            function toggleTheme() {
                const currentTheme = body.dataset.theme === 'dark' ? 'light' : 'dark';
                applyTheme(currentTheme);
            }

            function initialize() {
                videoElement.addEventListener('play', () => { 
                    userPaused = false; 
                });
                
                videoElement.addEventListener('playing', () => {
                    userPaused = false;
                    if (stallTimer) clearTimeout(stallTimer);
                    updatePlaybackStatus('播放中', 'var(--success-color)');
                    retryCount = 0;
                });
                
                videoElement.addEventListener('pause', () => {
                    userPaused = true;
                    if (stallTimer) clearTimeout(stallTimer);
                    if (!videoElement.ended) {
                        updatePlaybackStatus('已暂停', 'var(--text-muted)');
                    }
                });

                videoElement.addEventListener('waiting', () => {
                    updatePlaybackStatus('缓冲中', 'orange');
                    if (userPaused) return; 

                    if (stallTimer) clearTimeout(stallTimer); 
                    stallTimer = setTimeout(() => {
                        if (userPaused) return; 
                        console.log('Playback stalled for too long. Forcing reconnect...');
                        updatePlaybackStatus('停滞, 重连', 'orange');
                        playChannel(currentPlayingUrl);
                    }, 8000); 
                });
                
                videoElement.addEventListener('ended', () => {
                    if (isCurrentStreamLive && currentPlayingUrl && !userPaused) {
                        console.log('Live stream ended unexpectedly. Retrying...');
                        updatePlaybackStatus('直播中断, 重连', 'orange');
                        playChannel(currentPlayingUrl);
                    } else if (!isCurrentStreamLive) {
                        updatePlaybackStatus('已结束', '#6c757d');
                    }
                });
                
                sourceSelector.addEventListener('change', handleSourceSelect);
                saveSourceBtn.addEventListener('click', saveCustomSource);
                deleteSourceBtn.addEventListener('click', deleteCustomSource);
                loadListBtn.addEventListener('click', loadM3UFromUrl);
                playDirectBtn.addEventListener('click', handleDirectPlay);
                localFileInput.addEventListener('change', handleFileSelect);
                searchInput.addEventListener('input', debounce(handleSearch, 300));
                checkAvailabilityBtn.addEventListener('click', toggleCheckAvailability);
                exportBtn.addEventListener('click', handleExport);
                filterAllBtn.addEventListener('click', () => handleFilterChange('all'));
                filterAvailableBtn.addEventListener('click', () => handleFilterChange('available'));
                filterUnavailableBtn.addEventListener('click', () => handleFilterChange('unavailable'));
                playlistToggleBtn.addEventListener('click', handlePlaylistToggle);
                themeToggleBtn.addEventListener('click', toggleTheme);
                
                populateSourceSelector();
                
                updateModeStatus('未加载', '#6c757d');
                updatePlaybackStatus('待机', '#6c757d');
                updateCheckStatus('未检测', '#6c757d');

                const savedTheme = localStorage.getItem(STORAGE_KEY_THEME) || 'dark';
                applyTheme(savedTheme);

                tippy('[data-tippy-content]', { animation: 'scale-subtle', theme: 'translucent',placement: 'bottom' });
            }

            initialize();
        });
    </script>
</body>
</html>